using System;
using System.Collections.Generic;

namespace AreaGenerator
{
	public class Area
	{
		private Node[,] nodes;
		public int length;
		public int width;
		
		private static int MAX_ROOM_SIZE = 6;
		
		private HashSet<Room> rooms;
		private RoomTile[,] tiles;
		private HashSet<Wall> doors;
		
		public Random random = new Random();
		
		public Area(int width, int length){
			this.length = length;
			this.width = width;
			nodes = new Node[width,length];
			rooms = new HashSet<Room>();
			doors = new HashSet<Wall>();
			tiles = new RoomTile[width-1,length-1];
			
			for (int i = 0 ; i < width ; i++){
				for (int j = 0 ; j < length ; j++){
					nodes[i,j] = new Node(i,j);
				}
			}
			
			for (int i = 0 ; i < width ; i++){
				for (int j = 0 ; j < length ; j++){
					Node n = (j!=length-1)?nodes[i,j+1]:null;
					Node s = (j!=0)?nodes[i,j-1]:null;
					Node w = (i!=0)?nodes[i-1,j]:null;
					Node e = (i!=width-1)?nodes[i+1,j]:null;
					
					nodes[i,j].setAdjacentNodes(n, s, e, w);
				}
			}
			
			
			
			for (int i = 0 ; i < width-1 ; i++){
				for (int j = 0 ; j < length-1 ; j++){
					RoomTile tile = new RoomTile(nodes[i,j]);
					tiles[i,j] = tile;
					Room room = new Room(tile);
					tile.setRoom(room);
					room.ID = i * (length -1) + j;
					rooms.Add(room);
				}
			}
			
			for (int i = 0 ; i < width-1 ; i++){
				for (int j = 0 ; j < length-1 ; j++){
					if (j!=0) setWall(nodes[i,j], nodes[i+1,j]);
					if (i!=0) setWall(nodes[i,j], nodes[i,j+1]);
				}
			}
		}
		
		public void setWall(Node n1, Node n2){
			//détermine l'adjacence de 2 rooms par un mur
			if (!n1.isAdjacent(n2)) return;
			Room room1 = null;
			Room room2 = null;
			get2RoomFromWall(new Wall(n1,n2), ref room1, ref room2, false);
			if (room1 != null && room2 != null){
				room1.addAdjacentRoom(room2);
				room2.addAdjacentRoom(room1);
			}
		}
		
		public bool removeWall(Node n1, Node n2){
			if (!n1.isAdjacent(n2)) return false;
			Room room1 = null;
			Room room2 = null;
			get2RoomFromWall(new Wall(n1,n2), ref room1, ref room2, false);
			
			//cas particulier où le mur sépare 2 tile dans la même room
			if (room2 == null) {
				int k = 0;
				foreach (RoomTile tile in room1.getTiles()){
					if (tile.containsNode(n1) && tile.containsNode(n2)) k++;
				}
				
				if (k == 2) 
				{
					room1.addNoWall(n1, n2);
					room1.getWalls().Remove(new Wall(n1, n2));
					return true;
				}
				else return false;
			}
			
			//si la fusion des 2 rooms serait trop importante, on annule
			if (room1.size() + room2.size() > MAX_ROOM_SIZE) return false;
			
			Wall separator = new Wall(n1, n2);
			//fusion des 2 rooms
			Room room = new Room(room1, room2, separator);
			rooms.Remove(room1);
			rooms.Remove(room2);
			rooms.Add(room);
	 		
			return true;
			
		}
		
		public void setDoors(){
			//donne des valeurs aux rooms
			Room entrance = null;
			Room exit = null;
			foreach (Room room in rooms){
				room.setValue(random.Next(50));
				foreach (RoomTile tile in room.getTiles()){
					if (tile.isEntrance()) entrance = room;
					if (tile.isExit()) exit = room;
				}
			}
			
			if (entrance == null || exit == null){
				Console.WriteLine("setDoors error");
				return;
			}
			
			//recherche du chemin à la valeur la plus faible
			Dictionary<int, int> pathValue = new Dictionary<int, int>();
			foreach (Room room in rooms){
				pathValue.Add(room.ID, int.MaxValue);
			}
			pathValue[entrance.ID] = entrance.getValue();
			
			Dictionary<int, Room> previousRoom = new Dictionary<int, Room>();
			
			HashSet<Room> roomPool = new HashSet<Room>(rooms);
			HashSet<Room> markedRooms = new HashSet<Room>();
			
			while(roomPool.Count != 0){
				Room room = null;
				int minValue = int.MaxValue;
				foreach (Room r in roomPool){
					if (pathValue[r.ID] < minValue){
						room = r;
						minValue = pathValue[r.ID];
					}
				}
				markedRooms.Add(room);
				roomPool.Remove(room);
				
				foreach (Room nextRoom in room.getAdjacentRooms()){
					if (!markedRooms.Contains(nextRoom)){
						if (minValue + nextRoom.getValue() < pathValue[nextRoom.ID] ){
							pathValue[nextRoom.ID] = minValue + nextRoom.getValue();
							previousRoom.Add(nextRoom.ID, room);
						}
					}
				}
			}
			
			//construction du chemin
			List<Room> path = new List<Room>();
			Room currentRoom = exit;
			while(!currentRoom.isEntrance()){
				path.Add(currentRoom);
				currentRoom = previousRoom[currentRoom.ID];
			}
			path.Add(currentRoom);
			
			
			for(int i = 0 ; i < path.Count-1 ; i++){
				addDoor(path[i], path[i+1]);
			}
			
			//enlève les rooms qui ne font pas parti du chemin
			foreach (Room room in rooms){
				if (!path.Contains(room)){
					room.setEnabled(false);
				}
			}
			
			//trouve les rooms enlevées mais adjacentes aux rooms actives
			HashSet<Room> adjRooms = new HashSet<Room>();
			foreach (Room room in path){
				foreach (Room adj in room.getAdjacentRooms()){
					if(!adjRooms.Contains(adj)) adjRooms.Add(adj);
				}
			}
			
			//ajoute x rooms facultatives + plus des useless portes
			for (int i = 0 ; i < 10 ; i++){
				Room room = path[random.Next(path.Count)];
				Room adj = room.getAdjacentRoomsAsList()[random.Next(room.getAdjacentRooms().Count)];
				
				if (adj.isEnabled()) i--;
				else adj.setEnabled(true);
				
				path.Add(adj);
				addDoor(room, adj);
			}
			
			//ajoute x rooms qui seront accessibles grâce à des "clés"
			for (int i = 1 ; i < 3 ; i++){
				Room room = path[random.Next(path.Count)];
				Room adj = room.getAdjacentRoomsAsList()[random.Next(room.getAdjacentRooms().Count)];
				
				if (adj.isEnabled()){
					i--;
				}
				else 
				{
					adj.setEnabled(true);
					addDoor(room, adj).keyID = i;
					adj.keyID = i;
					Room keyRoom = path[random.Next(path.Count)];
					RoomTile keyTile = keyRoom.getTilesAsList()[random.Next(keyRoom.getTiles().Count)];
					keyTile.keyID = i;
					path.Add(adj);
					//solution pour rendre la salle accessible seulement par la porte :
					//faire que la salle ne soit pas "deep"
					//mettre toutes les salles adjacentes à la même hauteur
					//si possible, faire en sorte qu'il n'y ait qu'une salle adjacente
				}
			}
		}
		
		public void removeRandomWalls(int n){
			for (int i = 0 ; i < n ; i++){
				Node n1 = nodes[random.Next(width),random.Next(length)];
				int dir = random.Next(4);
				Node n2 = null;
				switch (dir){
					case 0 :
						n2 = n1.north;
						break;
					case 1 :
						n2 = n1.south;
						break;
					case 2 :
						n2 = n1.east;
						break;
					case 3 :
						n2 = n1.west;
						break;
				}
				if (n2 != null){
					if (!removeWall(n1, n2)) i--;
				}
				else i--;
				
				
			}
		}
		
		public Wall addDoor(Room room1, Room room2){
			HashSet<Node> nodes1 = new HashSet<Node>();
			HashSet<Node> nodes2 = new HashSet<Node>();
			
			foreach (RoomTile tile in room1.getTiles()){
				nodes1.Add(tile.ne);
				nodes1.Add(tile.se);
				nodes1.Add(tile.sw);
				nodes1.Add(tile.nw);
			}
			
			foreach (RoomTile tile in room2.getTiles()){
				nodes2.Add(tile.ne);
				nodes2.Add(tile.se);
				nodes2.Add(tile.sw);
				nodes2.Add(tile.nw);
			}
			
			HashSet<Node> sharedNodes = new HashSet<Node>();
			
			foreach (Node node in nodes1){
				if (nodes2.Contains(node)){
					sharedNodes.Add(node);
				}
			}
			
			HashSet<Wall> walls = new HashSet<Wall>();
			
			foreach (Node node1 in sharedNodes){
				foreach (Node node2 in sharedNodes){
					if (node1.isAdjacent(node2)){
						Wall wall = new Wall(node1, node2);
						if (room1.containsAbstractWall(wall) && room2.containsAbstractWall(wall) && !walls.Contains(wall)) walls.Add(wall);
					}
				}
			}
			
			int r = random.Next(walls.Count);
			int i = 0;
			Wall res = null;
			foreach (Wall wall in walls){
				if (i == r) {
					doors.Add(wall);
					room1.getWalls().Remove(wall);
					room2.getWalls().Remove(wall);
					res = wall;
					break;
				}
				i++;
			}
			return res;
		}
		
		public void setGround(){
			foreach (Room room in rooms){
				bool high = (random.Next(2) == 1);
				bool deep = (random.Next(2) == 1);
				room.setGround(high, deep);
			}
		}
		
		public void setWallTypes(){
			Room leftRoom;
			Room rightRoom;
			foreach(Wall w in getWalls()){
				leftRoom = null;
				rightRoom = null;
				get2RoomFromWall(w, ref leftRoom, ref rightRoom, true);
				w.setWallType(leftRoom, rightRoom);
			}
		}
		
		public void setDoorTypes(){
			Room leftRoom;
			Room rightRoom;
			foreach(Wall w in getDoors()){
				leftRoom = null;
				rightRoom = null;
				get2RoomFromWall(w, ref leftRoom, ref rightRoom, true);
				w.setDoorType(leftRoom, rightRoom);
			}
			
		}
			
		public void get2RoomFromWall(Wall w, ref Room leftRoom, ref Room rightRoom, bool oriented){
			leftRoom = null;
			rightRoom = null;
			foreach (Room r in rooms){
				if (r.containsNode(w.n1) && r.containsNode(w.n2)){
					if (leftRoom == null) leftRoom = r;
					else if (rightRoom == null) rightRoom = r;
				}
			}
			
			if (oriented){
				RoomTile leftTile = null;
				RoomTile rightTile = null;
				get2TilesFromWall(w, ref leftTile, ref rightTile);
				if ((rightRoom != null && rightRoom.getTiles().Contains(leftTile))
					||(leftRoom != null && leftRoom.getTiles().Contains(rightTile))){
					Room tmp = leftRoom;
					leftRoom = rightRoom;
					rightRoom = tmp;
				}
			}
		}
		
		public void get2TilesFromWall(Wall w, ref RoomTile leftTile, ref RoomTile rightTile){
			leftTile = null;
			rightTile = null;
			foreach (RoomTile t in tiles){
				if (t.containsNode(w.n1) && t.containsNode(w.n2)){
					if (t.isLeftTile(w)) leftTile = t;
					else rightTile = t;
				}
			}	
		}
		
		public void setEntrance(int x, int y){
			tiles[x,y].setEntrance(true);
		}
		
		public void setExit(int x, int y){
			tiles[x,y].setExit(true);
		}
		
		public HashSet<Room> getRooms(){
			return rooms;
		}
		
		public HashSet<Wall> getDoors(){
			HashSet<Wall> hashset = new HashSet<Wall>(doors);
			return new HashSet<Wall>(hashset);
		}
		
		public HashSet<Wall> getWalls(){
			HashSet<Wall> res = new HashSet<Wall>();
			foreach (Room r in rooms){
				if (r.isEnabled()){
					foreach (Wall w in r.getWalls()){
						if (!res.Contains(w)) res.Add(w);
					}
				}
			}
			return res;
		}
		
		public HashSet<Wall> getNoWalls(){
			HashSet<Wall> res = new HashSet<Wall>();
			foreach (Room r in rooms){
				if (r.isEnabled()){
					foreach (Wall w in r.getNoWalls()){
						if (!res.Contains(w)) res.Add(w);
					}
				}
			}
			return res;
		}
		
		public HashSet<Node> getEnabledNodes(){
			HashSet<Node> res = new HashSet<Node>();
			foreach (Room r in rooms){
				if (r.isEnabled()){
					foreach (Node n in r.getNodes()){
						if (!res.Contains(n)) res.Add(n);
					}
				}
			}
			return res;
		}
		
		public bool isDeepNode(Node n){
			foreach(Room room in rooms){
				if (!room.isDeep() && room.containsNode(n) && room.isEnabled()) return false;
			}
			return true;
		}
		
		public HashSet<Wall> getNode4Walls(Node n){
			HashSet<Wall> res = new HashSet<Wall>();
			foreach(Wall wall in getWalls()){
				if (wall.containsNode(n)) res.Add(wall);
			}
			return res;
		}
		
		public Wall getWall(Node n1, Node n2){
			foreach(Wall wall in getNoWalls()){
				if (wall.Equals(new Wall(n1, n2))) return wall;
			}
			foreach(Wall wall in getWalls()){
				if (wall.Equals(new Wall(n1, n2))) return wall;
			}
			return null;
		}
		
		public RoomTile getTile(Node node, diagOrientation ori){
			foreach(RoomTile tile in tiles){
				if (ori == diagOrientation.ne && node.Equals(tile.sw)) return tile;
				if (ori == diagOrientation.se && node.Equals(tile.nw)) return tile;
				if (ori == diagOrientation.sw && node.Equals(tile.ne)) return tile;
				if (ori == diagOrientation.nw && node.Equals(tile.se)) return tile;
			}
			return null;
		}
		
		public Node[,] getNodes(){
			return nodes;
		}
		
		public String toString(){
			return rooms.ToString();
		}
	}
	
	public enum diagOrientation{
		ne,
		se,
		sw,
		nw
	}
}

